﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ServiceStack.Text;
using ServiceStack.VirtualPath;

namespace ServiceStack.IO;

public class FileSystemVirtualFiles
    : AbstractVirtualPathProviderBase, IVirtualFiles
{
    public DirectoryInfo RootDirInfo { get; protected set; }
    protected FileSystemVirtualDirectory RootDir;

    public override IVirtualDirectory RootDirectory => RootDir;
    public override string VirtualPathSeparator => "/";
    public override string RealPathSeparator => Convert.ToString(Path.DirectorySeparatorChar);

    public FileSystemVirtualFiles(string rootDirectoryPath)
        : this(new DirectoryInfo(rootDirectoryPath))
    {
    }

    public FileSystemVirtualFiles(DirectoryInfo rootDirInfo)
    {
        this.RootDirInfo = rootDirInfo ?? throw new ArgumentNullException(nameof(rootDirInfo));
        Initialize();
    }

    protected sealed override void Initialize()
    {
        if (!RootDirInfo.Exists)
            throw new Exception($"RootDir '{RootDirInfo.FullName}' for virtual path does not exist");

        RootDir = new FileSystemVirtualDirectory(this, null, RootDirInfo);
    }

    public override bool DirectoryExists(string virtualPath)
    {
        var isDirectory = Directory.Exists(RootDirectory.RealPath.CombineWith(SanitizePath(virtualPath)));
        return isDirectory;
    }

    public override bool FileExists(string virtualPath)
    {
        var isFile = File.Exists(RootDirectory.RealPath.CombineWith(SanitizePath(virtualPath)));
        return isFile;
    }

    public string EnsureDirectory(string dirPath)
    {
        if (!Directory.Exists(dirPath))
            Directory.CreateDirectory(dirPath);

        return dirPath;
    }

    public void WriteFile(string filePath, string textContents)
    {
        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        EnsureDirectory(Path.GetDirectoryName(realFilePath));
        File.WriteAllText(realFilePath, textContents);
    }

    public void WriteFile(string filePath, Stream stream)
    {
        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        EnsureDirectory(Path.GetDirectoryName(realFilePath));
        using var fs = File.Open(realFilePath, FileMode.Create, FileAccess.Write);
        stream.WriteTo(fs);
    }

    public override async Task WriteFileAsync(string filePath, object contents, CancellationToken token=default)
    {
        if (contents == null)
            return;

        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        EnsureDirectory(Path.GetDirectoryName(realFilePath));
        using var fs = File.Open(realFilePath, FileMode.Create, FileAccess.Write);

        if (contents is IVirtualFile vfile)
            await WriteFileAsync(filePath, vfile.GetContents(), token).ConfigAwait();
        else if (contents is string textContents)
            await fs.WriteAsync(textContents, token).ConfigAwait();
        else if (contents is ReadOnlyMemory<char> romChars)
            await fs.WriteAsync(romChars.Span, token).ConfigAwait();
        else if (contents is byte[] binaryContents)
            await fs.WriteAsync(binaryContents, token: token).ConfigAwait();
        else if (contents is ReadOnlyMemory<byte> romBytes)
            await fs.WriteAsync(romBytes, token).ConfigAwait();
        else if (contents is Stream stream)
            await stream.CopyToAsync(fs, token).ConfigAwait();
        else
            throw CreateContentNotSupportedException(contents);
    }

    public void WriteFiles(IEnumerable<IVirtualFile> files, Func<IVirtualFile, string> toPath = null)
    {
        this.CopyFrom(files, toPath);
    }

    public void AppendFile(string filePath, string textContents)
    {
        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        EnsureDirectory(Path.GetDirectoryName(realFilePath));
        File.AppendAllText(realFilePath, textContents);
    }

    public void AppendFile(string filePath, Stream stream)
    {
        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        EnsureDirectory(Path.GetDirectoryName(realFilePath));
        using (var fs = new FileStream(realFilePath, FileMode.Append))
        {
            stream.WriteTo(fs);
        }
    }

    public void DeleteFile(string filePath)
    {
        var realFilePath = RootDir.RealPath.CombineWith(filePath);
        try
        {
            File.Delete(realFilePath);
        }
        catch (Exception /*ignore*/)
        {
        }
    }

    public void DeleteFiles(IEnumerable<string> filePaths)
    {
        filePaths.Each(DeleteFile);
    }

    public void DeleteFolder(string dirPath)
    {
        var realPath = RootDir.RealPath.CombineWith(dirPath);
#if NETCORE
            // Doesn't properly recursively delete nested dirs/files on .NET Core (win at least)
            if (Directory.Exists(realPath))
                DeleteDirectory(realPath);
#else
        if (Directory.Exists(realPath))
            Directory.Delete(realPath, recursive: true);
#endif
    }
        
    public static void DeleteDirectory(string path)
    {
        if (!Directory.Exists(path))
            return;
            
        //modified from https://stackoverflow.com/a/1703799/85785
        foreach (var directory in Directory.GetDirectories(path))
        {
            var files = Directory.GetFiles(directory);
            foreach (var file in files)
            {
                File.SetAttributes(file, FileAttributes.Normal);
            }

            DeleteDirectory(directory);
        }

        try
        {
            Directory.Delete(path, true);
        }
        catch (IOException) 
        {
            Directory.Delete(path, true);
        }
        catch (UnauthorizedAccessException)
        {
            Directory.Delete(path, true);
        }
    }
        
    public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
    {
        if (string.Equals(source.FullName, target.FullName, StringComparison.CurrentCultureIgnoreCase))
            return;

        // Check if the target directory exists, if not, create it.
        if (!Directory.Exists(target.FullName))
        {
            Directory.CreateDirectory(target.FullName);
        }

        // Copy each file into it's new directory.
        foreach (var file in source.GetFiles())
        {
            file.CopyTo(Path.Combine(target.ToString(), file.Name), true);
        }

        // Copy each subdirectory using recursion.
        foreach (var sourceSubDir in source.GetDirectories())
        {
            var nextTargetSubDir = target.CreateSubdirectory(sourceSubDir.Name);
            CopyAll(sourceSubDir, nextTargetSubDir);
        }
    }        

    public static string AssertDirectory(string dirPath, int timeoutMs=1000)
    {
        if (string.IsNullOrEmpty(dirPath))
            return null;

        try
        {
            ExecUtils.RetryOnException(() => {
                if (!Directory.Exists(dirPath))
                    Directory.CreateDirectory(dirPath);
            }, TimeSpan.FromMilliseconds(timeoutMs));
            return dirPath;
        }
        catch (TimeoutException e)
        {
            throw e.InnerException ?? e;
        }
    }

    public static void RecreateDirectory(string dirPath, int timeoutMs = 1000)
    {
        if (Directory.Exists(dirPath))
        {
            DeleteDirectory(dirPath);
        }
        AssertDirectory(dirPath, timeoutMs);
    }

    /// <summary>
    /// Validates that a relative path is safe and doesn't traverse outside the intended directory.
    /// </summary>
    /// <param name="basePath">The base directory path.</param>
    /// <param name="relativePath">The relative path to validate.</param>
    /// <returns>True if the path is safe, false otherwise.</returns>
    public static bool IsPathSafe(string basePath, string relativePath)
    {
        try
        {
            // Normalize paths to handle different directory separators
            basePath = Path.GetFullPath(basePath);
                    
            // Combine the base path with the relative path
            string fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));
                    
            // Check if the resulting path starts with the base path
            return fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase);
        }
        catch (Exception ex) when (ex is ArgumentException || 
                                   ex is NotSupportedException || 
                                   ex is PathTooLongException)
        {
            // Path contains invalid characters or is in an invalid format
            return false;
        }
    }
}